# -----------------------------------------------------------------------------
# Copyright (c) 2025, Southeast University (China)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#  - Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  - Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#  - Neither the name of the copyright holders nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Authors: Jiajie Xu, Yanfang Zhang, Jiaqi Gao, Leyun Tian
# -----------------------------------------------------------------------------


import linecache
import os
import sys
import re
from tqdm import tqdm

class Delay:
    def __init__(self):
        self.index1 = [] # input transition
        self.index2 = [] # output capacitance
        self.delay = []
    def __repr__(self):
        delay_str = "\n".join(map(str, self.delay[0:]))
        return f"\nindex1:\n{self.index1}\nindex2:\n{self.index2}\nDelay:\n{delay_str}\n"

class Trans:
    def __init__(self):
        self.index1 = []
        self.index2 = []
        self.trans = []
        self.isscalar = False # for specific type of cell with scalar type
    def __repr__(self):
        trans_str = "\n".join(map(str, self.trans[0:]))
        return f"\nindex1:\n{self.index1}\nindex2:\n{self.index2}\nTrans:\n{trans_str}\n"

class Constrain:
    def __init__(self):
        self.index1 = []
        self.index2 = []
        self.constrain = []
    def __repr__(self):
        Constrain_str = "\n".join(map(str, self.constrain[0:]))
        return f"\nindex1:\n{self.index1}\nindex2:\n{self.index2}\nConstrain:\n{Constrain_str}\n"

class Cell_Timing:
    def __init__(self):
        self.delay = {}
        self.trans = {}
        self.constrain = {} # constrained time
        self.name = ''
        self.footprint = ''
        self.pins = []
        self.receive_cap = {} # (pin, r/f) -> caps
    def __repr__(self):
        return (
        "------------------------------------------ Cell ------------------------------------------\n"
        f"CellName: {self.name}\n"
        f"footprint: {self.footprint}\n"
        f"pins: {self.pins}\n"
        f"receiver capacitance: {self.receive_cap}\n"
        "------------------------------------------ Delay ------------------------------------------\n"
        f"{self.delay}\n"
        "------------------------------------------ Trans ------------------------------------------\n"
        f"{self.trans}\n"
        "----------------------------------------- Constrain ----------------------------------------\n"
        f"{self.constrain}"
    )

def Read_TimingLib(inlib, position = 1, results=None):
    """Read NLDM model from timing libs, return dict of cells"""
    cells = {}
    if not os.path.exists(inlib):
        raise FileNotFoundError(f"Timing library file not found: {inlib}")
    
    file_size = os.stat(inlib).st_size
    # set the unit map and default unit
    unit_map_time = {'1ns': 1e-9,'1ps': 1e-12,'1ms': 1e-3,'1us': 1e-6}
    unit_map_currnet = {'1A': 1, '1mA': 1e-3,'1uA': 1e-6}
    unit_map_caps = {'nf': 1e-9, 'pf': 1e-12, 'mf': 1e-3, 'uf': 1e-6}
    default_unit_time = '1ns'
    default_unit_currnet = '1mA'
    default_unit_caps = 'pf'
    # set scaler for units
    current_scaler = 1
    time_scaler = 1
    caps_scaler = 1
    with tqdm(total=file_size, 
            unit="B", 
            unit_scale=True, 
            desc=f"Processing {inlib.split('/')[-1]}",
            ncols=130, 
            colour="cyan", 
            bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [Elapsed: {elapsed} | Speed: {rate_fmt}]", 
            position=position, 
            leave=True, 
            dynamic_ncols=False,
            file=sys.stdout) as pbar:
        with open(inlib, 'r') as infile:
            linecount = 0
            for line in infile:
                pbar.update(len(line.encode('utf-8')))
                linecount += 1
                index = line.split()
                # read unit of lib
                if(len(index) >= 3):
                    if index[0] == "current_unit":
                        unit_index = index[2].replace('"', '')
                        if unit_index not in unit_map_currnet.keys():
                            raise ValueError("Unrecognized current unit! please check the library.")
                        else:
                            current_scaler = current_scaler * unit_map_currnet[unit_index]/unit_map_currnet[default_unit_currnet]
                    if index[0] == "time_unit":
                        unit_index = index[2].replace('"', '')
                        if unit_index not in unit_map_time.keys():
                            raise ValueError("Unrecognized time unit! please check the library.")
                        else:
                            time_scaler = time_scaler * unit_map_time[unit_index]/unit_map_time[default_unit_time]
                    if index[0] == "capacitive_load_unit":
                        unit_index = index[1].split(',')[1].replace(')', '')
                        if unit_index not in unit_map_caps.keys():
                            raise ValueError("Unrecognized capacitance unit! please check the library.")
                        else:
                            caps_scaler = caps_scaler * unit_map_caps[unit_index]/unit_map_caps[default_unit_caps]

                # cell insertation
                if(len(index) >= 3):
                    if(index[0] == 'cell' and '(' in index[1] and ')' in index[1] and index[2] == '{'):
                        try:
                            newcell
                        except NameError:
                            pass
                        else:
                            cells[newcell.name] = newcell
                        # new cell found
                        newcell = Cell_Timing()
                        newcell.name = index[1].replace('(', '').replace(')', '')
                        # initialize sense
                        #cell_names.append(index[1].replace('(', '').replace(')', ''))
                if(len(index) >= 3):
                    # footprint
                    if(index[0] == 'cell_footprint'):
                        newcell.footprint = index[2].replace('"', '').replace(';', '')
                if(len(index) >= 2):
                    # new pin found
                    if(index[0] == 'pin' and '(' in index[1] and ')' in index[1] and index[2] == '{') or (index[0].find('pin(') != -1):
                        if len(index) == 3:
                            newpin = index[1].replace('(', '').replace(')', '')
                        else:
                            newpin = index[0].replace('pin(', '').replace(')', '')
                        newcell.pins.append(newpin)
                        outpin = newpin
                    # recive capacitance info
                    if(index[0] == 'rise_capacitance'):
                        newcell.receive_cap[(newpin, 'r')] = float(index[2].replace(";","")) * caps_scaler
                    if(index[0] == 'fall_capacitance'):
                        newcell.receive_cap[(newpin, 'f')] = float(index[2].replace(";","")) * caps_scaler
                    # timing info
                    if(index[0] == 'timing'):
                        # Initialize sdf condition and when with *
                        sense = "*"
                        sdf_cond = '*'
                        when = '*'
                        next_line = linecache.getline(inlib, linecount + 1)
                        next_index = next_line.split()
                        if(len(next_index) >= 3):
                            if(next_index[0] == 'related_pin'):
                                inpin = next_index[2].replace('"', '').replace(';', '')
                    # timing sense represents if the input signal share the same transition direction with output signal
                    if(index[0] == 'timing_sense'):
                        sense = index[2].replace(';', '')
                    if(index[0] == 'timing_type'):
                        type = index[2].replace(';', '')
                    if(index[0] == 'sdf_cond'):
                        matches = re.findall(r'"(.*?)"', line)
                        if matches:
                            # concatenate all matches
                            result = ''.join(matches)
                            sdf_cond = result.replace(" ", "")
                    if(index[0] == 'when'):
                        when = index[2].replace(';', '').replace('"', '')
                    if(index[0] == 'cell_rise'):
                        rf = 'r'
                    elif(index[0] == 'cell_fall'):
                        rf = 'f'
                    if(index[0] == 'cell_rise' or index[0] == 'cell_fall'):
                        newdelay = Delay()
                        arc = inpin + '->' + outpin
                        template = re.findall(r'\d+', index[1])
                        index1_length = int(template[0])
                        index1_line = linecache.getline(inlib, linecount + 1)
                        index1_index = index1_line.split()
                        for i in range(index1_length):
                            newdelay.index1.append(float(index1_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', '')) * time_scaler)
                        index2_length = int(template[1])
                        index2_line = linecache.getline(inlib, linecount + 2)
                        index2_index = index2_line.split()
                        for i in range(index2_length):
                            newdelay.index2.append(float(index2_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', '')) * caps_scaler)
                        for j in range(index2_length):
                            delay_line = linecache.getline(inlib, linecount + j + 4)
                            delay_index = delay_line.split()
                            delays = []
                            for i in range(index1_length):
                                delays.append(float(delay_index[i].replace('"', '').replace(',', '')) * time_scaler)
                            newdelay.delay.append(delays)
                        newcell.delay[(arc,rf,sense,type,when,sdf_cond)] = newdelay
                    # transition info
                    if(index[0] == 'rise_transition'):
                        rf = 'r'
                    elif(index[0] == 'fall_transition'):
                        rf = 'f'
                    if(index[0] == 'rise_transition' or index[0] == 'fall_transition'):
                        newtrans = Trans()
                        arc = inpin + '->' + outpin
                        if(index[1] == '(scalar)'):
                            newtrans.index1 = None
                            newtrans.index2 = None
                            newtrans.isscalar = True
                            newtrans.trans = 0
                            continue
                        template = re.findall(r'\d+', index[1])
                        index1_length = int(template[0])
                        index1_line = linecache.getline(inlib, linecount + 1)
                        index1_index = index1_line.split()
                        for i in range(index1_length):
                            newtrans.index1.append(float(index1_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', ''))  * time_scaler)
                        index2_length = int(template[1])
                        index2_line = linecache.getline(inlib, linecount + 2)
                        index2_index = index2_line.split()
                        for i in range(index2_length):
                            newtrans.index2.append(float(index2_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', '')) * caps_scaler)
                        for j in range(index2_length):
                            trans_line = linecache.getline(inlib, linecount + j + 4)
                            trans_index = trans_line.split()
                            transes = []
                            for i in range(index1_length):
                                transes.append(float(trans_index[i].replace('"', '').replace(',', ''))  * time_scaler)
                            newtrans.trans.append(transes)
                        newcell.trans[(arc,rf,sense,type,when,sdf_cond)] = newtrans
                    # constraint info
                    if(index[0] == 'rise_constraint'):
                        rf = 'r'
                    elif(index[0] == 'fall_constraint'):
                        rf = 'f'
                    if(index[0] == 'rise_constraint' or index[0] == 'fall_constraint'):
                        # clock pule is 1D lut
                        if type != 'min_pulse_width':
                            newconstrain = Constrain()
                            arc = inpin + '->' + outpin
                            template = re.findall(r'\d+', index[1])
                            index1_length = int(template[0])
                            index1_line = linecache.getline(inlib, linecount + 1)
                            index1_index = index1_line.split()
                            for i in range(index1_length):
                                newconstrain.index1.append(float(index1_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', ''))  * time_scaler)
                            index2_length = int(template[1])
                            index2_line = linecache.getline(inlib, linecount + 2)
                            index2_index = index2_line.split()
                            for i in range(index2_length):
                                newconstrain.index2.append(float(index2_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', '')) * time_scaler)
                            for j in range(index2_length):
                                cons_line = linecache.getline(inlib, linecount + j + 4)
                                cons_index = cons_line.split()
                                conses = []
                                for i in range(index1_length):
                                    conses.append(float(cons_index[i].replace('"', '').replace(',', ''))  * time_scaler)
                                newconstrain.constrain.append(conses)    
                        else:
                            newconstrain = Constrain()
                            arc = inpin + '->' + outpin
                            template = re.findall(r'\d+', index[1])
                            index1_length = int(template[0])
                            index1_line = linecache.getline(inlib, linecount + 1)
                            index1_index = index1_line.split()
                            for i in range(index1_length):
                                newconstrain.index1.append(float(index1_index[1+i].replace('(', '').replace('"', '').replace(')', '').replace(';', '').replace(',', ''))  * time_scaler)
                            cons_line = linecache.getline(inlib, linecount + 3)
                            cons_index = cons_line.split()
                            for i in range(index1_length):
                                newconstrain.constrain.append(float(trans_index[i].replace('"', '').replace(',', ''))  * time_scaler)
                        newcell.constrain[(arc,rf,sense,type,when,sdf_cond)] = newconstrain
            cells[newcell.name] = newcell
        # tqdm.write(f"{inlib.split('/')[-1]} Readed.")
    if results != None:
        results[position] = cells
    return cells


if __name__ == "__main__":
    import pickle
    root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.append(root_dir)
    import Global_var
    lib_dir = os.path.join(Global_var.work_dir, "libs")
    inlib = os.path.join(lib_dir, "tcbn12ffcllbwp16p90ssgnp0p63v125c_ccs.lib")
    cells = Read_TimingLib(inlib)
    #with open("./1", "wb") as f:
    #   pickle.dump(cells ,f)
    #print(f'cells = {cells}') 
    print(f'cells.items = {cells.items}')
    #for cellname, cell_Timing in cells.items():
    #    print(cellname)
    #    print(f'{cellname} : Delay = {cell_Timing.delay}')
    #    for lut_name , _ , _ in cell_Timing.delay:
            #print(f'lut_name = {lut_name}')